| 单元 | 核心目标(每单元≤3项) | 中途检查 |
|---|---|---|
| A|质量 | 字段、缺失、重复 | 写出清洗前后核对 |
| B|分布 | 中心、离散、形态 | 用统计量核对一张分布图 |
| C|时间 | 月度聚合、季节性、边界 | 改变时间粒度做敏感性 |
价格弹性与产品共现为课后挑战:必须有交易级授权数据与因果识别设计才可形成业务结论;不完成挑战不影响核心学习要求。
先作答:列出交易表六个必需字段,并写出清洗前后行数核对式。
反馈:合格结果含 order_id/order_date/category/quantity/price/sales_amount,且满足“原始行数=清洗行数+移除行数”;缺字段或清洗后为空即停止。
先作答:选择一个数值字段,用中心、离散、形态三类统计量解释一张分布图。
反馈:均值/中位数回答中心,标准差回答离散,偏度回答形态;异常原因不能由偏度或箱线图单独推出。
先作答:把订单聚合为月度表,再改为季度表,预测行数和峰值月份是否变化。
反馈:两表必须来自同一清洗快照并报告订单分母;粒度改变会合并波动,不能把聚合差异解释为季节因果。
价格弹性需价格外生变动或识别设计,产品共现需交易级授权和重复购买定义;挑战答案须另报假设、偏差与需要重新检查的情况,不能替代核心质量—分布—时间依据。
农产品电子商务的快速发展产生了海量交易数据,通过可视化分析可以:
EDA 由统计学家 John Tukey 于 1977 年提出,核心思想是在正式建模前理解数据特征:
| 原则 | 说明 |
|---|---|
| 怀疑精神 | 对数据和假设保持质疑 |
| 可视化优先 | 图形比数字更直观 |
| 计算支撑 | 用统计量验证视觉发现 |
| 迭代探索 | 分析是循环往复的过程 |
农产品数据与一般商品数据相比,具有四大显著特征:
processed_data.csv 由教学平台提供,用于固定练习;公开教材的拓展案例使用课程下载的上市公司基本信息和财务报表。以下代码与教学平台任务要求完全一致:
| 阶段 | 学习步骤 | 应得到什么 |
|---|---|---|
| 1 数据要求 | 核对文件、字段、期间、单位与授权 | 数据字典与行数 |
| 2 质量检查 | 检查缺失、重复、类型与极端值 | 质量报告 |
| 3 单变量分布 | 描述年龄、品类、渠道等分布 | 带分母的统计表 |
| 阶段 | 学习步骤 | 应得到什么 |
|---|---|---|
| 4 时间结构 | 检查趋势、季节性与断点 | 时间图与窗口说明 |
| 5 关系验证 | 先可视化,再用统计量核对 | 观测、假设与局限 |
data、feature_map、category_mapping、order 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。# 注:processed_data.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入必要的数据分析和可视化库
import pandas as pd
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
import seaborn as sns # 导入Seaborn可视化库
import warnings # 导入warnings模块用于控制警告输出
warnings.filterwarnings('ignore') # 忽略运行时警告信息以保持输出整洁
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体作为默认字体以支持中文
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
# 读取处理好的数据集
data = pd.read_csv('processed_data.csv')
# 查看数据集的基本情况
print(data.head()) # 显示数据集前几行
print("查看数据集的基本信息:") # 输出查看数据集的基本信息
print(data.info()) # 显示数据集的基本信息,包括列名、数据类型和非空值数量
print("查看数据集有无缺失值:") # 输出查看数据集有无缺失值
print(data.isnull().sum()) # 统计每列的缺失值数量
print("查看数据集有无重复值:") # 输出查看数据集有无重复值
print(data.duplicated().sum()) # 统计重复行的数量
# 创建特征映射字典,将英文列名映射为中文名称,用于图表标题
feature_map = {
'price': '价格', # 映射"price"→"价格"
'quantity': '数量', # 映射"quantity"→"数量"
'customer_age': '顾客年龄', # 映射"customer_age"→"顾客年龄"
'return_flag': '是否返单', # 映射"return_flag"→"是否返单"
'sales_amount': '销售额' # 映射"sales_amount"→"销售额"
} # 数据结构定义结束
# 创建箱型图展示各特征的分布情况
plt.figure(figsize=(20,10)) # 设置图形大小
for i, feature in enumerate(feature_map.keys()): # 循环遍历
plt.subplot(2,3, i+1) # 创建2行3列的子图布局
plt.title(f'{feature_map[feature]}的箱型图') # 设置子图标题
plt.boxplot(data[feature]) # 绘制箱线图
plt.xlabel(f'{feature_map[feature]}') # 设置x轴标签
plt.ylabel('频率') # 设置y轴标签
plt.savefig("特征箱型图.png") # 保存图形
# 创建一个字典来映射产品名称到正确的分类
category_mapping = {
'安溪铁观音': '茶叶', # 映射"安溪铁观音"→"茶叶"
'武夷岩茶': '茶叶', # 映射"武夷岩茶"→"茶叶"
'福州茉莉花': '茶叶', # 映射"福州茉莉花"→"茶叶"
'古田银耳': '食用菌', # 映射"古田银耳"→"食用菌"
'建宁莲子': '中药材', # 映射"建宁莲子"→"中药材"
'琯溪蜜柚': '水果', # 映射"琯溪蜜柚"→"水果"
'宁德大黄鱼': '水产品' # 映射"宁德大黄鱼"→"水产品"
} # 数据结构定义结束
# 应用映射来更新category列,将产品名称转换为产品类别
data['category'] = data['product_name'].map(category_mapping).fillna(data['category'])
# 2.可视化展示 - 计算并转置描述性统计数据(结果未打印,仅计算)
data.describe().T
# 2.1 顾客信息可视化
plt.figure(figsize=(20,10)) # 设置图形大小
# 顾客年龄分布直方图
plt.subplot(2,2,1)
sns.distplot(data['customer_age'], bins=30, kde=True, color='blue') # 绘制带核密度估计的直方图
plt.title('顾客年龄分布', fontsize=16) # 设置标题
plt.xlabel('顾客年龄', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
# 顾客年龄组别分布柱状图
plt.subplot(2,2,2)
order = ['18-25', '26-35', '36-45', '46-55', '56+'] # 自定义年龄组顺序
sns.countplot(data=data, x='age_group', color='orange', order=order) # 按指定顺序绘制柱状图
plt.title('顾客年龄组别分布', fontsize=16) # 设置标题
plt.xlabel('顾客年龄组别', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 顾客性别分布柱状图
plt.subplot(2,2,3)
sns.countplot(data=data, x='customer_gender', color='green') # 绘制性别分布柱状图
plt.title('顾客性别分布', fontsize=16) # 设置标题
plt.xlabel('顾客性别', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 顾客性别比例饼图
plt.subplot(2,2,4)
plt.pie(data['customer_gender'].value_counts(), # 绘制饼图
labels=data['customer_gender'].value_counts().index, # 统计各个值的出现频次
autopct='%1.1f%%', colors=['green', 'orange']) # 绘制饼图并显示百分比
plt.title('顾客性别比例', fontsize=16) # 设置标题
plt.axis('equal') # 确保饼图是圆形
plt.savefig("顾客信息.png") # 保存图形
#运行后依据计数表报告年龄组别样本量与占比;当前不预填结论
#运行后依据计数表报告性别样本量、占比与缺失情况;当前不预填结论
# 区域分布可视化
plt.figure(figsize=(20,5)) # 设置图形大小
sns.countplot(x='region', data=data, palette='Set2') # 绘制计数柱状图
plt.title('区域分布', fontsize=20) # 设置标题
plt.xlabel('区域', fontsize=16) # 设置x轴标签
plt.ylabel('数量', fontsize=16) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
plt.savefig("区域分布.png") # 保存图形
# 2.2 商品信息可视化
plt.figure(figsize=(20,5)) # 设置图形大小
# 产品名称分布柱状图
plt.subplot(1, 2, 1)
sns.countplot(x='product_name', data=data, palette='Set2') # 绘制产品名称分布柱状图
plt.xlabel('产品名称', fontsize=16) # 设置x轴标签
plt.ylabel('数量', fontsize=16) # 设置y轴标签
plt.title('产品分布', fontsize=20) # 设置标题
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 产品类型分布柱状图
plt.subplot(1, 2, 2)
sns.countplot(x='category', data=data, palette='Set2') # 绘制产品类型分布柱状图
plt.xlabel('产品类型', fontsize=16) # 设置x轴标签
plt.ylabel('数量', fontsize=16) # 设置y轴标签
plt.title('产品类型分布', fontsize=20) # 设置标题
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
plt.savefig("商品信息.png") # 保存图形
#总体而言,茶叶是销量最多的产品类型,远超其他产品类型。
#具体产品分布上,销量最多的产品是古田银耳,共1474单;其次是安溪铁观音,共1461单;第三名是宁德大黄鱼,共1438单。
# 销售相关指标分布可视化
plt.figure(figsize=(20,6)) # 设置图形大小
# 价格分布直方图
plt.subplot(1, 3, 1)
sns.distplot(data['price'], kde=True, color='blue') # 绘制价格分布直方图
plt.title('价格分布', fontsize=16) # 设置标题
plt.xlabel('价格', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
# 数量分布直方图
plt.subplot(1, 3, 2)
sns.distplot(data['quantity'], kde=True, color='green') # 绘制数量分布直方图
plt.title('数量分布', fontsize=16) # 设置标题
plt.xlabel('数量', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
# 销售额分布直方图
plt.subplot(1, 3, 3)
sns.distplot(data['sales_amount'], kde=True, color='orange') # 绘制销售额分布直方图
plt.title('销售额分布', fontsize=16) # 设置标题
plt.xlabel('销售额', fontsize=12) # 设置x轴标签
plt.ylabel('频率', fontsize=12) # 设置y轴标签
plt.savefig("销售.png") # 保存图形
# 2.3 平台及优惠信息可视化
plt.figure(figsize=(20,12)) # 设置图形大小
# 平台分布柱状图
plt.subplot(2, 3, 1)
sns.countplot(x='channel', data=data, palette='Set2') # 绘制平台分布柱状图
plt.title('平台分布', fontsize=16) # 设置标题
plt.xlabel('平台', fontsize=14) # 设置x轴标签
plt.ylabel('频数', fontsize=14) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 平台比例饼图
plt.subplot(2, 3 ,2)
plt.pie(data['channel'].value_counts(), # 绘制饼图
labels=data['channel'].value_counts().index, # 统计各个值的出现频次
autopct='%1.1f%%') # 绘制平台比例饼图
plt.title('平台比例', fontsize=16) # 设置标题
# 优惠情况分布柱状图
plt.subplot(2,3, 3)
sns.countplot(x='promotion', data=data, palette='Set2') # 绘制优惠情况分布柱状图
plt.title('优惠情况分布', fontsize=16) # 设置标题
plt.xlabel('优惠情况', fontsize=14) # 设置x轴标签
plt.ylabel('频数', fontsize=14) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 优惠情况比例饼图
plt.subplot(2, 3, 4)
plt.pie(data['promotion'].value_counts(), # 绘制饼图
labels=data['promotion'].value_counts().index, # 统计各个值的出现频次
autopct='%1.1f%%') # 绘制优惠情况比例饼图
plt.title('优惠情况比例', fontsize=16) # 设置标题
# 是否返单分布柱状图
plt.subplot(2, 3, 5)
sns.countplot(x='return_flag', data=data, palette='Set2') # 绘制是否返单分布柱状图
plt.title('是否返单分布', fontsize=16) # 设置标题
plt.xlabel('是否返单', fontsize=14) # 设置x轴标签
plt.ylabel('频数', fontsize=14) # 设置y轴标签
# 在每个柱子上方添加具体数值
for p in plt.gca().patches:
plt.gca().annotate(format(p.get_height(), '.0f'), # 获取当前坐标轴并在柱状图顶部标注数值
(p.get_x() + p.get_width() / 2., p.get_height()), # 设置标注位置为柱子顶部中心
# 设置标注文本的水平对齐、垂直偏移等显示参数
ha='center', va='center', xytext=(0, 5), textcoords='offset points')
# 是否返单比例饼图
plt.subplot(2, 3, 6)
plt.pie(data['return_flag'].value_counts(), # 绘制饼图
labels=data['return_flag'].value_counts().index, # 统计各个值的出现频次
autopct='%1.1f%%') # 绘制是否返单比例饼图
plt.title('是否返单比例', fontsize=16) # 设置标题
plt.savefig("平台及优惠信息.png") # 保存图形
#运行后依据平台计数表报告排序、样本量与占比;当前不预填结论
#运行后依据优惠计数表报告各类别样本量与占比;当前不预填结论
#运行后依据返单计数表报告样本量、占比与缺失情况;当前不预填结论运行后核对:核对 data、feature_map、category_mapping、order 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。
拓展练习:把数据替换为一个中国企业或市场序列,改变一个分组或时间窗口;说明图形结论是否改变,以及为何。
数据预处理是分析的第一步,通过查看数据结构和基本统计信息来了解数据质量:
shape:返回数据维度(行数, 列数)info():显示每列的数据类型和非空值数量describe().T:转置描述统计表,更易阅读isnull().sum():统计每列缺失值数量import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False
# 读取数据并输出基本信息
data = pd.read_csv('processed_data.csv')
print('数据形状:', data.shape)
print('\n数据前5行:')
print(data.head())
print('\n数据信息:')
print(data.info())
print('\n描述统计:')
print(data.describe().T)在初步检查中,需要关注三类数据类型:
| 数据类型 | 示例字段 | 说明 |
|---|---|---|
| 数值型 (int/float) | 年龄、金额、数量 | 可直接参与计算 |
| 字符型 (object) | 产品名称、类别 | 需要编码或分组分析 |
| 日期型 (datetime) | 下单日期 | 需要格式转换后提取特征 |
在 Matplotlib 中显示中文需要两步配置:
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']:设置思源宋体为默认中文字体plt.rcParams['axes.unicode_minus'] = False:修复负号显示问题产品分类是电商数据分析的基础,农产品通常采用混合分类策略:
map() 函数的核心机制map(dict):根据字典进行键值映射fillna():处理未匹配的值(保留原分类)for 循环映射高效 100 倍以上drop_duplicates():去重查看唯一映射关系缺失值分为三种:
| 类型 | 含义 | 处理方法 |
|---|---|---|
| MCAR | 完全随机缺失,与任何变量无关 | 直接删除或插补 |
| MAR | 缺失与观测变量相关 | 多重插补 |
| MNAR | 缺失与未观测变量相关 | 建模机制 |
# 缺失值统计
missing_counts = data.isnull().sum()
missing_pct = (missing_counts / len(data)) * 100
missing_df = pd.DataFrame({
'缺失数': missing_counts,
'缺失比例': missing_pct
})
print(missing_df)
# 缺失值可视化
plt.figure(figsize=(10, 6))
missing_df['缺失比例'].plot(kind='bar', color='steelblue', alpha=0.7)
plt.title('各字段缺失值比例', fontsize=14)
plt.xlabel('字段名', fontsize=12)
plt.ylabel('缺失比例(%)', fontsize=12)
plt.xticks(rotation=45)
plt.axhline(y=5, color='red', linestyle='--', label='5%阈值')
plt.legend()
plt.tight_layout()
plt.show()异常值 (Outlier) 指显著偏离其他观测值的数据点:
| 方法类别 | 具体方法 | 适用场景 |
|---|---|---|
| 基于统计量 | 3σ 原则、IQR 方法 | 正态/近似正态分布 |
| 基于可视化 | 箱线图、散点图 | 快速直观判断 |
| 基于模型 | DBSCAN、孤立森林 | 高维复杂数据 |
IQR 方法的判定规则:
超出上下界的数据点即为异常值。
# 选择数值型字段
numeric_cols = data_clean.select_dtypes(include=[np.number]).columns
# 绘制箱线图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()
for i, col in enumerate(numeric_cols[:4]):
data_clean.boxplot(column=col, ax=axes[i])
axes[i].set_title(f'{col} - 箱线图', fontsize=12)
axes[i].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# IQR 异常值检测函数
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return ((series < lower) | (series > upper)).sum()
# 输出异常值统计
print('\n异常值统计(IQR方法):')
for col in numeric_cols:
outlier_count = detect_outliers_iqr(data_clean[col].dropna())
print(f'{col}: {outlier_count}个异常值')| 维度 | 指标 | 含义 |
|---|---|---|
| 集中趋势 | 均值、中位数、众数 | 数据的中心位置 |
| 离散程度 | 方差、标准差、极差 | 数据偏离中心的程度 |
| 分布形状 | 偏度、峰度 | 对称性和尖锐程度 |
| 指标 | 负值 | 约为 0 | 正值 |
|---|---|---|---|
| 偏度 | 左尾更长 | 近似对称 | 右尾更长 |
| 超额峰度 | 尾部较轻 | 接近正态 | 尾部较重 |
判读边界:偏度不等于“存在异常值”,峰度也不等于因果机制;须结合直方图、箱线图与数据口径核对。
from scipy.stats import skew, kurtosis
sales = data_clean['sales_amount']
# 绘制分布图
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 直方图(含均值和中位数参考线)
axes[0].hist(sales, bins=50, color='steelblue', alpha=0.7,
edgecolor='black')
axes[0].axvline(sales.mean(), color='red', linestyle='--',
linewidth=2, label=f'均值={sales.mean():.2f}')
axes[0].axvline(sales.median(), color='green', linestyle='--',
linewidth=2, label=f'中位数={sales.median():.2f}')
axes[0].set_title('销售额分布直方图', fontsize=14)
axes[0].set_xlabel('销售额(元)', fontsize=12)
axes[0].set_ylabel('频数', fontsize=12)
axes[0].legend(fontsize=10)
# 箱线图
axes[1].boxplot(sales, vert=True)
axes[1].set_title('销售额箱线图', fontsize=14)
axes[1].set_ylabel('销售额(元)', fontsize=12)
plt.tight_layout()
plt.show()
# 输出分布统计量
print(f'均值: {sales.mean():.2f}')
print(f'中位数: {sales.median():.2f}')
print(f'标准差: {sales.std():.2f}')
print(f'偏度: {skew(sales):.4f}')
print(f'峰度: {kurtosis(sales):.4f}')# 按品类聚合统计
category_sales = data_clean.groupby('category')['sales_amount'].agg([
'sum', 'count', 'mean'
])
category_sales.columns = ['总销售额', '订单数', '平均单价']
category_sales = category_sales.sort_values('总销售额', ascending=False)
print('\n品类销售统计:')
print(category_sales)
# 可视化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 总销售额柱状图
category_sales['总销售额'].plot(kind='bar', ax=axes[0],
color='steelblue', alpha=0.7)
axes[0].set_title('各品类总销售额', fontsize=14)
axes[0].set_xlabel('产品类别', fontsize=12)
axes[0].set_ylabel('销售额(元)', fontsize=12)
axes[0].tick_params(axis='x', rotation=45)
# 订单数饼图
axes[1].pie(category_sales['订单数'],
labels=category_sales.index,
autopct='%1.1f%%', startangle=90,
colors=sns.color_palette('Set3'))
axes[1].set_title('各品类订单数占比', fontsize=14)
plt.tight_layout()
plt.show()数据可用后,按以下顺序识别品类结构:
| 特征 | 说明 | 示例 |
|---|---|---|
| 趋势性 | 长期上升或下降趋势 | 年销售额逐年增长 |
| 季节性 | 固定周期的波动 | 春节前后销量高峰 |
| 周期性 | 不固定周期的波动 | 经济周期影响消费 |
| 随机性 | 不可预测的噪声 | 突发事件造成波动 |
# 确保日期格式正确
data_clean['下单日期'] = pd.to_datetime(data_clean['order_date'])
# 提取时间特征
data_clean['年'] = data_clean['下单日期'].dt.year
data_clean['月'] = data_clean['下单日期'].dt.month
data_clean['日'] = data_clean['下单日期'].dt.day
data_clean['星期'] = data_clean['下单日期'].dt.dayofweek
data_clean['小时'] = data_clean['下单日期'].dt.hour
# 按月统计
monthly_sales = data_clean.groupby('月')['sales_amount'].agg([
'sum', 'count'
])
monthly_sales.columns = ['月销售额', '月订单数']
print('月度销售统计:')
print(monthly_sales)fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 月销售额折线图
monthly_sales['月销售额'].plot(ax=axes[0], marker='o', linewidth=2)
axes[0].set_title('月销售额趋势', fontsize=14)
axes[0].set_xlabel('月份', fontsize=12)
axes[0].set_ylabel('销售额(元)', fontsize=12)
axes[0].grid(True, alpha=0.3)
# 月订单数柱状图
monthly_sales['月订单数'].plot(kind='bar', ax=axes[1],
color='coral', alpha=0.7)
axes[1].set_title('月订单数分布', fontsize=14)
axes[1].set_xlabel('月份', fontsize=12)
axes[1].set_ylabel('订单数', fontsize=12)
plt.tight_layout()
plt.show()分析工作日与周末的消费差异,为营销和物流策略提供依据:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 平均销售额对比
weekend_comparison['mean'].plot(kind='bar', ax=axes[0],
color=['skyblue', 'orange'], alpha=0.7)
axes[0].set_title('平均销售额对比', fontsize=14)
axes[0].set_ylabel('平均销售额(元)', fontsize=12)
axes[0].tick_params(axis='x', rotation=0)
# 总销售额对比
weekend_comparison['sum'].plot(kind='bar', ax=axes[1],
color=['skyblue', 'orange'], alpha=0.7)
axes[1].set_title('总销售额对比', fontsize=14)
axes[1].set_ylabel('总销售额(元)', fontsize=12)
axes[1].tick_params(axis='x', rotation=0)
plt.tight_layout()
plt.show()价格弹性 (Price Elasticity) 衡量需求量对价格变化的敏感度:
\[ \large E_p = \frac{\%\Delta Q}{\%\Delta P} = \frac{\partial Q}{\partial P} \times \frac{P}{Q} \]
下方是不同产品之间的截面相关分析,不是价格弹性估计。产品异质性、促销、季节和供货可能共同影响价格与销量;需要同一产品的价格变化、控制变量和可辩识的价格冲击才能向弹性推进。
# 按产品统计价格与销量
product_stats = data_clean.groupby('product_name').agg({
'sales_amount': 'sum',
'quantity': 'sum',
'unit_price': 'mean'
})
product_stats['单价'] = (product_stats['sales_amount']
/ product_stats['quantity'])
# 计算相关系数
corr_coef = product_stats[['单价', 'quantity']].corr().iloc[0, 1]
print(f'价格与销量的相关系数: {corr_coef:.4f}')fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 散点图
axes[0].scatter(product_stats['单价'],
product_stats['quantity'], s=100, alpha=0.6)
axes[0].set_title(f'价格-销量散点图 (r={corr_coef:.4f})', fontsize=14)
axes[0].set_xlabel('单价(元)', fontsize=12)
axes[0].set_ylabel('销量', fontsize=12)
# 添加产品标签
for idx, row in product_stats.iterrows():
axes[0].annotate(idx, (row['单价'], row['quantity']), fontsize=8)
# 热力图
sns.heatmap(product_stats[['单价', 'quantity']].corr(),
annot=True, cmap='coolwarm', center=0, ax=axes[1],
cbar_kws={'label': '相关系数'})
axes[1].set_title('相关性热力图', fontsize=14)
plt.tight_layout()
plt.show()产品共现分析可以发现哪些产品经常被一起购买:
from itertools import combinations
# 获取每个订单的产品列表
order_products = data_clean.groupby('order_id')[
'product_name'
].apply(list)
# 统计产品对共现次数
co_occurrence = {}
for products in order_products:
for combo in combinations(sorted(products), 2):
co_occurrence[combo] = co_occurrence.get(combo, 0) + 1
# 转换为 DataFrame 并排序
co_df = pd.DataFrame(list(co_occurrence.items()),
columns=['产品对', '共现次数'])
co_df = co_df.sort_values('共现次数', ascending=False).head(10)
print('Top 10产品组合:')
print(co_df)plt.figure(figsize=(12, 6))
plt.barh(range(len(co_df)), co_df['共现次数'],
color='steelblue', alpha=0.7)
plt.yticks(range(len(co_df)),
[f'{p1} + {p2}' for p1, p2 in co_df['产品对']])
plt.xlabel('共现次数', fontsize=12)
plt.title('产品共现分析(Top 10)', fontsize=14)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()| 分析环节 | 核心方法 | 关键工具 |
|---|---|---|
| 数据准备 | 分类映射、类型转换 | map(), pd.to_datetime() |
| 质量检查 | 缺失值、异常值检测 | isnull(), IQR 方法 |
| 分布分析 | 偏度、峰度、直方图 | scipy.stats, hist() |
| 时间分析 | 月度趋势、周末效应 | dt 属性、groupby() |
| 多变量 | 相关分析、共现分析 | corr(), combinations |
quantity/price/sales_amount 输出 count、mean、median、std、skew,并用其中两个统计量核对一张分布图。order_id/order_date/category/quantity/price/sales_amountimport pandas as pd # 导入表格库
def analyze_platform_agriculture(agri_transactions): # 定义平台农产品EDA完整接口
required={'order_id','order_date','category','quantity','price','sales_amount'} # 对齐平台实际字段
if not required<=set(agri_transactions.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 缺列时暂停分析
raw_rows=len(agri_transactions) # 记录清洗前行数
clean_transactions=agri_transactions.dropna(subset=list(required)).drop_duplicates().copy() # 去重并排除关键字段缺失
if clean_transactions.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 空样本时停止
clean_transactions['order_date']=pd.to_datetime(clean_transactions['order_date']) # 统一交易日期
clean_transactions['month']=clean_transactions['order_date'].dt.to_period('M').astype(str) # 构造月度键
clean_transactions['quarter']=clean_transactions['order_date'].dt.to_period('Q').astype(str) # 由同一清洗快照构造季度键
if clean_transactions['month'].nunique()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 时间样本不足时停止
reconciliation={'raw_rows':raw_rows,'clean_rows':len(clean_transactions),'removed_rows':raw_rows-len(clean_transactions)} # 形成清洗核对
distribution=clean_transactions[['quantity','price','sales_amount']].agg(['count','mean','median','std','skew']) # 汇总分布五指标
monthly=clean_transactions.groupby('month').agg(orders=('order_id','nunique'),sales=('sales_amount','sum')) # 生成时间趋势
quarterly=clean_transactions.groupby('quarter').agg(orders=('order_id','nunique'),sales=('sales_amount','sum')) # 用相同字段口径生成季度趋势
relationship=clean_transactions[['quantity','price','sales_amount']].corr(method='spearman') # 计算稳健秩相关
category_check=clean_transactions.groupby('category')['sales_amount'].sum() # 独立复算品类销售
weekend_check=clean_transactions.assign(weekend=clean_transactions['order_date'].dt.dayofweek.ge(5)).groupby('weekend')['sales_amount'].agg(['count','sum','mean']) # 复算周末与工作日
assert reconciliation['raw_rows']==reconciliation['clean_rows']+reconciliation['removed_rows'] # 核对清洗行数守恒
if monthly['orders'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 分母异常时停止
assert monthly['sales'].sum()==quarterly['sales'].sum()==clean_transactions['sales_amount'].sum() # 核对月季销售额与清洗快照总额守恒
return reconciliation,distribution,monthly,quarterly,relationship,category_check,weekend_check # 返回质量、分布与月季时间结果def select_latest_disclosure(frame,value_fields): # 统一财报披露版本选择约定
required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns] # 固定键与可接受版本字段
if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 缺列时停止
work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce') # 清理必需字段
for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce') # 解析披露日期
keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum()) # 统计选择前重复
if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 无版本依据时停止
work['_version']=work[versions].max(axis=1) if versions else pd.NaT # 合并双版本字段为排序键
latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index) # 为每个公司报告期定位最晚披露时点
latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy() # 仅保留单例或并列最晚版本
tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum()) # 统计并列最晚版本行
conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0 # 统计并列且数值冲突的披露键
if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 并列最新值冲突时请先检查数据后再继续
selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore') # 仅合并数值完全一致的并列行
after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions} # 返回并列与冲突检查
if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 选择后仍重复则停止
return selected,audit # 返回唯一版本与检查依据from pathlib import Path # 导入路径工具绑定规定资产
import pandas as pd # 导入表格库以标准化HDF字段
import numpy as np # 导入有限值检查工具
root=Path('/home/ubuntu/r2_data_mount/data/stock') # 指定股票数据目录
basic_path,financial_path=root/'stock_basic_data.h5',root/'financial_statement.h5' # 绑定可用HDF基础信息与财报
if not basic_path.exists() or not financial_path.exists(): raise FileNotFoundError('未找到课程股票数据文件,请从课程数据下载入口获取') # 提示读者下载文件
basic=pd.read_hdf(basic_path,key='stock_basic_info',columns=['order_book_id','symbol','industry_name','province']).rename(columns={'order_book_id':'raw_code','symbol':'name','industry_name':'industry','province':'area'}) # 只读取公司筛选字段并标准化
basic['ts_code']=basic['raw_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False) # 统一证券代码后缀
basic['area']=basic['area'].astype('string').str.replace(r'[省市]$','',regex=True) # 统一省市简称
required_basic={'ts_code','name','industry','area'} # 定义公司字段结构
required_financial={'ts_code','end_date','revenue','operate_profit'} # 定义财报字段结构
agriculture=basic.loc[basic['industry'].str.contains('农|林|牧|渔',na=False) & basic['area'].isin(['上海','江苏','浙江','安徽'])] # 筛选长三角农业企业
if agriculture.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 空对象时停止
agriculture_raw_codes=agriculture['raw_code'].tolist() # 保留HDF原始代码用于高效行筛选
financial=pd.read_hdf(financial_path,key='financial_data',where='order_book_id in agriculture_raw_codes',columns=['order_book_id','quarter','info_date','operating_revenue','profit_from_operation']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'revenue','profit_from_operation':'operate_profit'}) # 按对象读取并标准化财报字段
financial['ts_code']=financial['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False) # 统一财报证券代码后缀
financial['end_date']=pd.PeriodIndex(financial['end_date'].str.upper(),freq='Q').end_time.normalize() # 将季度键转换为报告期末日期
if not required_basic<=set(basic.columns) or not required_financial<=set(financial.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 标准化后仍缺列时停止
financial,version_audit=select_latest_disclosure(financial,['revenue','operate_profit']) # 统一保留最晚披露版本
panel=financial.merge(agriculture[['ts_code','name','area']],on='ts_code',how='inner',validate='many_to_one').copy() # 合并公司标签if agriculture.empty or panel.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 空对象或空连接时停止
if panel['end_date'].nunique()<4: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 期间不足时停止
if panel['revenue'].eq(0).any() or not np.isfinite(panel[['revenue','operate_profit']].to_numpy()).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 比率分母检查要求
panel['operating_margin']=panel['operate_profit']/panel['revenue'] # 计算经营关系指标
distribution=panel[['revenue','operate_profit','operating_margin']].agg(['count','mean','median','std','skew']) # 输出分布统计
time_summary=panel.groupby('end_date').agg(companies=('ts_code','nunique'),revenue=('revenue','sum'),median_margin=('operating_margin','median')) # 输出时间趋势与分母
relationship=panel[['revenue','operate_profit','operating_margin']].corr(method='spearman') # 输出关系统计
area_check=panel.groupby('area')['revenue'].sum() # 独立复算地区子集
company_check=panel.loc[panel['ts_code'].eq(panel['ts_code'].iloc[0])].set_index('end_date')['revenue'] # 独立复算首家公司时间序列
print(version_audit,distribution,time_summary,relationship,area_check,company_check) # 输出数据版本检查与全部新案例依据clean_transactions 同时生成月度与季度订单数/销售额,比较聚合后的行数与波动,并用“月销售额合计=季销售额合计=清洗快照总额”检查核对;这只改变时间粒度,不更换清洗口径。中国财报拓展应用仍只验证方法,不替代订单依据。case 路径是规定资产;只看图不报统计;删除异常不留规则[商业大数据分析与应用]